/*
 * Copyright (c) 2009, Luke Emrose
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the
 * distribution.  Neither the name of Luke Emrose Sole Trader nor the
 * names of its contributors may be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @file
 * code by Luke Emrose - evolutionarytheory@gmail.com
 *
 * fake_time.cpp
 *
 * Stand alone application to perform the pixel sample recombination phase to simulate
 * an actual 3Delight render entirely using OpenGL.
 *
 * Current file format is a little lazy - so this will be changed to hdf5 sooner rather than later
 */

//======================================================================================

// c++ library includes
#include <iostream>
#include <fstream>

//======================================================================================

// stl includes
#include <vector>
#include <string>

//======================================================================================

// 3delight includes
#include <pointcloud.h>
#include <ri.h>

//======================================================================================

// imath includes
#include <OpenEXR/ImathMatrix.h>
#include <OpenEXR/ImathVec.h>
#include <OpenEXR/ImathColor.h>

//======================================================================================

// OpenGL includes
#include <GLee.h>
// include OpenGL
#ifdef WIN32
	#define WIN32_LEAN_AND_MEAN
		#include <windows.h>
	#endif
	#if defined( __APPLE__ ) && defined( __MACH__ )
		#include <OpenGL/gl.h>	// Header File For The OpenGL32 Library
		#include <OpenGL/glu.h>	// Header File For The GLu32 Library
	#else
		#include <GL/gl.h>	// Header File For The OpenGL32 Library
		#include <GL/glu.h>	// Header File For The GLu32 Library
#endif
#include <SDL/SDL.h>

//======================================================================================

// log file object
std::ofstream log_file;

//======================================================================================

/**
 * @brief
 * utility class to facilitate printing of point cloud user variables
 */
struct ptc_vars_printer
{
	/**
	 * @brief
	 * constructor
	 * takes info about the user variables ready for printing
	 */
	ptc_vars_printer
	(
		int in_num_vars,
		char** in_var_names,
		char** in_var_types
	)
		:
		var_data_size( in_num_vars ),
		var_names( in_num_vars )
	{
		for( int i = 0; i < in_num_vars; ++i )
		{
			var_names[ i ] = in_var_names[ i ];
			std::string curr_type( in_var_types[ i ] );

			// gather the appropriate sizes for different user variable types
			if( curr_type == "float" )
			{
				var_data_size[ i ] = 1;
			}
			else if( curr_type == "point" )
			{
				var_data_size[ i ] = 3;
			}
			else if( curr_type == "vector" )
			{
				var_data_size[ i ] = 3;
			}
			else if( curr_type == "normal" )
			{
				var_data_size[ i ] = 3;
			}
			else if( curr_type == "color" )
			{
				var_data_size[ i ] = 3;
			}
			else if( curr_type == "matrix" )
			{
				var_data_size[ i ] = 16;
			}
		}
	}

	/**
	 * @brief
	 * function to take a user data array, and print it nicely
	 */
	void get_user_vars_string( std::ostream& out, const std::vector< RtFloat >& data )
	{
		size_t data_index = 0;

		// iterate through each user variable
		for( size_t curr_var_id = 0; curr_var_id < var_data_size.size(); ++curr_var_id )
		{
			// iterate through the components of each user variable
			for( RtInt i = 0; i < var_data_size[ curr_var_id ]; ++i )
			{
				out << ", " << var_names[ curr_var_id ] << " = " << data[ data_index++ ];
			}
		}
	}

private:
	std::vector< RtInt > var_data_size;
	std::vector< std::string > var_names;
};

//======================================================================================

/**
 * @brief
 * utility class for easy access to 3delight point cloud files
 */
struct point_cloud_reader
{
	/**
	 * @brief
	 * constructor that loads a 3delight point cloud
	 */
	point_cloud_reader( const std::string in_filename )
	{
		// try and read file
		ptc_file = PtcOpenPointCloudFile( in_filename.c_str(), 0, 0, 0 );

		// read in the header data
		if( good() )
		{
			RtInt success = PtcGetPointCloudInfo( ptc_file, "npoints", &num_points );
			log_file << "number of points = " << num_points << std::endl;

			// number of variables in the point cloud
			success = PtcGetPointCloudInfo( ptc_file, "nvars", &num_vars );
			log_file << "number of user variables = " << num_vars << std::endl;

			// size of the data required to hold the resulting point variables
			success = PtcGetPointCloudInfo( ptc_file, "datasize", &data_size );
			log_file << "number of floats required to store all user variables = " << data_size << std::endl << std::endl;

			// allocate memory for variables information
			var_names = new char*[ num_vars ];
			var_types = new char*[ num_vars ];

			// read in the names and types of the variables
			success = PtcGetPointCloudInfo( ptc_file, "varnames", var_names );
			success = PtcGetPointCloudInfo( ptc_file, "vartypes", var_types );
		}
	}

	/**
	 * @brief
	 * destructor
	 */
	~point_cloud_reader()
	{
		if( ptc_file != 0 )
		{
			PtcClosePointCloudFile( ptc_file );
			delete [] var_types;
			delete [] var_names;
		}
	}

	/**
	 * @brief
	 * take as input an empty vector of 3d points, and fill it with the points from this point cloud
	 */
	template < typename T >
	void get_points( std::vector< T >& points )
	{
		RtInt success;
		RtFloat p[ 3 ];
		RtFloat n[ 3 ];
		RtFloat r;
		std::vector< RtFloat > data( data_size );

		// print each point one by one
		for( int i = 0; i < num_points; ++i )
		{

			success = PtcReadDataPoint( ptc_file, p, n, &r, &( data[ 0 ] ) );
			if( success )
			{
				// add this point
				points.push_back( T( p[ 0 ], p[ 1 ], p[ 2 ] ) );
			}
		}
	}

	/**
	 * @brief
	 * test whether the file opened correctly
	 * this should be called to first to know if it is
	 * safe to call any of the other functions
	 */
	bool good( void )
	{
		return ( ptc_file != 0 );
	}

private:
	// point cloud file pointer
	PtcPointCloud ptc_file;

	// number of points stored within the point cloud
	RtInt num_points;

	// number of user variables stored within the point cloud
	RtInt num_vars;

	// number of floats need per point to store all of the user data
	RtInt data_size;

	// array of the user data variable names
	char** var_types;

	// array of the user data variable types
	char** var_names;
};

//======================================================================================

/**
 * @brief
 * utility class for easy access to indirect framebuffer files
 */
struct indirect_framebuffer_reader
{
	/**
	 * @brief
	 * constructor that loads an indirect framebuffer
	 */
	indirect_framebuffer_reader( const std::string in_filename )
	{
		// try and read file
		ifb_file.open( in_filename.c_str(), std::ios::in );

		std::string line, var_name;

		// read in the header data
		if( good() )
		{
			// top header data
			std::getline( ifb_file, line );
			log_file << line << std::endl;

			// read in the world to camera matrix
			ifb_file
				>> var_name
				>> world_to_camera[ 0 ][ 0 ]
				>> world_to_camera[ 0 ][ 1 ]
				>> world_to_camera[ 0 ][ 2 ]
				>> world_to_camera[ 0 ][ 3 ]
				>> world_to_camera[ 1 ][ 0 ]
				>> world_to_camera[ 1 ][ 1 ]
				>> world_to_camera[ 1 ][ 2 ]
				>> world_to_camera[ 1 ][ 3 ]
				>> world_to_camera[ 2 ][ 0 ]
				>> world_to_camera[ 2 ][ 1 ]
				>> world_to_camera[ 2 ][ 2 ]
				>> world_to_camera[ 2 ][ 3 ]
				>> world_to_camera[ 3 ][ 0 ]
				>> world_to_camera[ 3 ][ 1 ]
				>> world_to_camera[ 3 ][ 2 ]
				>> world_to_camera[ 3 ][ 3 ]
				;
			log_file << var_name << " = " << world_to_camera << std::endl;

			// read in the world to NDC matrix
			ifb_file
				>> var_name
				>> world_to_NDC[ 0 ][ 0 ]
				>> world_to_NDC[ 0 ][ 1 ]
				>> world_to_NDC[ 0 ][ 2 ]
				>> world_to_NDC[ 0 ][ 3 ]
				>> world_to_NDC[ 1 ][ 0 ]
				>> world_to_NDC[ 1 ][ 1 ]
				>> world_to_NDC[ 1 ][ 2 ]
				>> world_to_NDC[ 1 ][ 3 ]
				>> world_to_NDC[ 2 ][ 0 ]
				>> world_to_NDC[ 2 ][ 1 ]
				>> world_to_NDC[ 2 ][ 2 ]
				>> world_to_NDC[ 2 ][ 3 ]
				>> world_to_NDC[ 3 ][ 0 ]
				>> world_to_NDC[ 3 ][ 1 ]
				>> world_to_NDC[ 3 ][ 2 ]
				>> world_to_NDC[ 3 ][ 3 ]
				;
			log_file << var_name << " = " << world_to_NDC << std::endl;

			// read in the clipping planes
			ifb_file >> var_name >> near_clip >> far_clip;
			log_file << var_name << " = " << near_clip << " " << far_clip << std::endl;

			// read in the resolution
			ifb_file >> var_name >> width >> height;
			log_file << var_name << " = " << width << " " << height << std::endl;

			// read in pixel samples
			ifb_file >> var_name >> pixel_samples_x >> pixel_samples_y;
			log_file << var_name << " = " << pixel_samples_x << " " << pixel_samples_y << std::endl;
		}
	}

	/**
	 * @brief
	 * destructor
	 */
	~indirect_framebuffer_reader()
	{
		if( ifb_file.is_open() )
		{
			ifb_file.close();
		}
	}

	/**
	 * @brief
	 * accessor functions for the private internal variables
	 */
	const int get_width( void ) { return width; }
	const int get_height( void ) { return height; }
	const int get_pixel_samples_x( void ) { return pixel_samples_x; }
	const int get_pixel_samples_y( void ) { return pixel_samples_y; }

	/**
	 * @brief
	 * take as input an empty vector of 2d points, and fill it with the points from this indirect frame buffer
	 */
	template < typename T >
	void get_points( std::vector< T >& points )
	{
		// storage for fragment data
		std::string mpId_name, x_name, y_name, u_name, v_name, opacity_name, depth_name, thickness_name;

		// the micropolygon id of the micropolygon that this fragment relates to
		int mpId;

		// x, y in screen pixels, depth in z units, and unknown thickness parameter
		float x, y, u, v, depth, thickness;

		// opacity of this fragment
		Imath::Color3< float > opacity;

		// read in all fragments
		while( !ifb_file.eof() )
		{
			ifb_file
				>> mpId_name >> mpId
				>> x_name >> x
				>> y_name >> y
				>> u_name >> u
				>> v_name >> v
				>> opacity_name >> opacity.x >> opacity.y >> opacity.z
				>> depth_name >> depth
				>> thickness_name >> thickness
				;

			// add this fragment
			points.push_back( T( x, y ) );
		}
	}

	/**
	 * @brief
	 * test whether the file opened correctly
	 * this should be called to first to know if it is
	 * safe to call any of the other functions
	 */
	bool good( void )
	{
		return ( ifb_file.is_open() && ifb_file.good() );
	}

private:
	// indirect frame buffer file
	std::ifstream ifb_file;

	// image size and sampling values
	int width, height, pixel_samples_x, pixel_samples_y;

	// transform matrices to be able to convert fragments into different spaces
	Imath::Matrix44< float > world_to_camera, world_to_NDC;

	// near and far clipping planes of the camera used for the render
	float near_clip, far_clip;
};

/**
 * @brief
 * a general OpenGL initialization function.  Sets all of the initial parameters
 */
void init_gl( int width, int height )
{
	glViewport( 0, 0, width, height );
	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity();
	gluOrtho2D( 0.0, width, 0.0, height );

	glEnable( GL_TEXTURE_2D );
	glShadeModel( GL_SMOOTH );

	glPointSize( 1.0 );
	glDisable( GL_POINT_SMOOTH );
	//glHint( GL_POINT_SMOOTH_HINT, GL_NICEST );

	glEnable( GL_BLEND );
	glBlendFunc( GL_SRC_ALPHA, GL_ONE );
}

/**
 * @brief
 * print out the contents of a specified 3delight point cloud
 */
int main( int argc, char* argv[] )
{
	log_file.open( "log.txt", std::ios::out );

	// load the input files
	std::string point_cloud_filename;
	std::string indirect_framebuffer_filename;

	// get input filenames
	if( argc == 3 )
	{
		point_cloud_filename = argv[ 1 ];
		indirect_framebuffer_filename = argv[ 2 ];
	}
	else
	{
		log_file << "ERROR: Please specify point cloud and indirect framebuffer files to load." << std::endl;
		return 1;
	}

	// load the point cloud file
	point_cloud_reader ptc_wrapper( point_cloud_filename );
	if( !ptc_wrapper.good() )
	{
		log_file << "ERROR: specified point cloud file is invalid, please try again." << std::endl;
		return 1;
	}

	// load the indirect framebuffer file
	indirect_framebuffer_reader ifb_wrapper( indirect_framebuffer_filename );
	if( !ifb_wrapper.good() )
	{
		log_file << "ERROR: specified indirect framebuffer file is invalid, please try again." << std::endl;
		return 1;
	}

	// notify the user of success
	log_file << "Successfully loaded point cloud: " << point_cloud_filename << std::endl;
	log_file << "Successfully loaded indirect framebuffer: " << indirect_framebuffer_filename << std::endl;

	//======================================================================================
	// read in some points to draw
	//======================================================================================

	std::vector< Imath::Vec2< float > > points;
	std::vector< Imath::Vec2< float > >::const_iterator points_iter;
	ifb_wrapper.get_points( points );
	log_file << "num of read in points = " << points.size() << std::endl;

	//======================================================================================
	// at this point fire up some openGL goodness....

	//======================================================================================
	// setup the OpenGL state
	//======================================================================================

	// verify that the correct extensions exist to make this all work correctly
	// for render to texture
	if( !WGL_ARB_extensions_string )
	{
		log_file << "WGL_ARB_extensions_string not available...." << std::endl;

		// close the log file
		log_file.close();

		return 1;
	}
	if( !WGL_ARB_render_texture )
	{
		log_file << "WGL_ARB_render_texture not available...." << std::endl;

		// close the log file
		log_file.close();

		return 1;
	}
	if( !WGL_ARB_pbuffer )
	{
		log_file << "WGL_ARG_pbuffer not available...." << std::endl;

		// close the log file
		log_file.close();

		return 1;
	}
	if( !WGL_ARB_pixel_format )
	{
		log_file << "WGL_ARB_pixel_format not available...." << std::endl;

		// close the log file
		log_file.close();

		return 1;
	}

	const SDL_Surface* screen = 0;

	atexit( SDL_Quit );

	// initialize SDL for video output
	if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
	{
		log_file << "ERROR: Unable to initialize SDL: " << SDL_GetError() << std::endl;

		// close the log file
		log_file.close();

		exit( 1 );
	}

	// set openGL attributes
	SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 8 );
	SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 8 );
	SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 8 );
	SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 24 );
	SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );

	// create OpenGL screen
	int options = SDL_OPENGL | SDL_HWSURFACE | SDL_DOUBLEBUF;
	screen = SDL_SetVideoMode( ifb_wrapper.get_width(), ifb_wrapper.get_height(), 0, options );
	if( !screen )
	{
		log_file << "ERROR: Unable to create OpenGL screen: " << SDL_GetError() << std::endl;

		// close the log file
		log_file.close();

		SDL_Quit();
		exit( 2 );
	}

	int isDoubleBuffered = 0;
	int success = SDL_GL_GetAttribute( SDL_GL_DOUBLEBUFFER, &isDoubleBuffered );
	if( ( success < 0 ) || ( !isDoubleBuffered ) )
	{
		log_file << "ERROR: Unable to create Double Buffered OpenGL screen: " << SDL_GetError() << std::endl;

		// close the log file
		log_file.close();

		SDL_Quit();
		exit( 2 );
	}

	// set the title bar in environments that support it
	std::string name = "Luke Emrose - Fake Time Relighting Engine - v0.0.1";
	SDL_WM_SetCaption( name.c_str(), name.c_str() );

	//======================================================================================
	// initialize OpenGL
	//======================================================================================

	const int width = ifb_wrapper.get_width();
	const int height = ifb_wrapper.get_height();
	init_gl( width, height );

	//======================================================================================
	// setup the main loop
	//======================================================================================

	// get pixel samples
	const int pixel_samples_x = ifb_wrapper.get_pixel_samples_x();
	const int pixel_samples_y = ifb_wrapper.get_pixel_samples_y();

	// calculate the current fragment opacity
	// currently assumes box filtering on a 1x1 support, with no transparency
	const float opacity = 1.0f / ( float ) ( pixel_samples_x * pixel_samples_y );

	// keep looping until we are all finished
	bool done = false;
	while( !done )
	{
		// clear the open gl background
		glClear( GL_COLOR_BUFFER_BIT );

		//===========
		// draw data
		//===========
		glBegin( GL_POINTS );
		{
			// iterate through the fragments, blending them all together additively	
			for( points_iter = points.begin(); points_iter != points.end(); ++points_iter )
			{
				glColor4f( 1.0, 1.0, 1.0, opacity );
				glVertex2f( ( *points_iter ).x, height - ( *points_iter ).y );
			}
		}
		glEnd();

		// swap buffers to display, since we're double buffered.
		SDL_GL_SwapBuffers();

		SDL_Delay( 30 );
		{
			SDL_Event event;
			while( SDL_PollEvent( &event ) )
			{
				if( event.type == SDL_QUIT )
				{
					done = true;
				}
				if( event.type == SDL_KEYDOWN )
				{
					if ( event.key.keysym.sym == SDLK_ESCAPE )
					{
						done = true;
					}
				}
			}
		}
	}
	
	//======================================================================================
	// close everything....
	//======================================================================================

	//Quit SDL 
	SDL_Quit(); 

	// close the log file
	log_file.close();

	return 0;
}